//
// Created by guoshunw on 2021/3/12.
//

#include "OweOggAnalyzer.h"

int OweOggAnalyzer::parsingOpusSampleRate = 48000;

string OggPageHeader::toString() const {
    stringstream os;
    os << this->preHeader.toString();
    os << ", header size: " << dec << getSize() << endl;

    os << "segment table(size: " << segmentTable.size() << "): " << hex;
    for (auto &size: segmentTable) {
        os << "0x" << size << ", ";
    }

    return os.str();
}

size_t OggPageHeader::read(FILE *f) {

    // header size =  PRE_HEADER_SIZE + this->num_segments;

    auto read_size = fread(&this->preHeader, 1, PRE_HEADER_SIZE, f);
    if (read_size < 1) {
        return read_size;
    }
    // read segment table
    uint8_t segmentTab[preHeader.num_segments];
    read_size = fread(segmentTab, sizeof(segmentTab), 1, f);
    if (read_size < 1) {
        logError("segments less than 1: %ld", (long) read_size);
        return read_size;
    }
    size_t segmentSize{};
    segmentTable.clear();
    for (auto i = 0; i < preHeader.num_segments; ++i) {
        segmentSize += segmentTab[i];
        if (segmentTab[i] != 0xff) { // 0xff means a packet ends
            segmentTable.push_back(segmentSize);
            segmentSize = 0;
        }
    }

    return ftell(f);
}

string OggPage::toString() const {
    stringstream os;
    os << "index: " << this->pageIndex << ", start offset: 0x" << setfill('0') << setw(2) << hex << starOffset
       << ", size: " << dec
       << setw(4) << this->getSize() << "(" << hex << getSize() << ")";
    os << ", payload offset: 0x" << getPayloadOffset() << ", payload size: " << dec << getPayloadSize();

    os << endl;
    os << header.toString() << endl;

    os << "payload: ";
    if (PayloadType::OPUS_ID_HEADER == getPayloadType()) {
        auto header = getPayloadAsOpusIDHeader();
        OweOggAnalyzer::parsingOpusSampleRate = header->sample_rate;
        os << header->toString();
    } else if (PayloadType::OPUS_COMMENT_HEADER == getPayloadType()) {
        os << getPayloadAsOpusCommentHeader()->toString();
    } else {
        // this is ogg page payload to string
        os << payloadToString();
    }
    os << endl;
    os
            << "--------------------------------------------------------------------------------------------------------------";

    return os.str();
}

int OggPage::read(FILE *f) {
    // free the previous payload
    delete payload;
    memset(static_cast<void *>(this), 0, sizeof(*this));

    starOffset = ftell(f);
    auto ret = this->header.read(f);
    if (0 == ret) {
        logError("Read header error");
        return -1;
    }
//    logDebug("payload offset: %ld(0x%02lx), payload size: %ld(0x%02lx)", getPayloadOffset(), getPayloadOffset(),
//             getPayloadSize(), getPayloadSize());

    // skip payload

    payload = new unsigned char[getPayloadSize()];
    //fseek(f, getPayloadSize(), SEEK_CUR);//
    ret = fread((void *) payload, getPayloadSize(), 1, f);

    if (ret <= 0) {
        logInfo("End of file");
        return -1;
    }
//    logDebug("start offset: %d", starOffset);
    return starOffset;
}

string OggPage::payloadToString(size_t line_limit, size_t max_out) const {
    stringstream os;

#define DUMP_PACKETS
#ifndef DUMP_PACKETS
    os << hexdump(payload, max_out);
    if (max_out < getPayloadSize()) {
        os << "..." << endl;
    }
#else
    // dump packets

    int i = 0;
    int output_packets_limit = 3;
    auto packet = payload;
    os << "total packets: " << this->header.segmentTable.size() << endl;

    for (auto size: this->header.segmentTable) {
        os << "packet" << i++ << "(size: " << size << ")";
        // if it's a opus packet, dump it
//        os << Utils::hexDump(packet, size) << endl;
        if (PayloadType::PACKET_DATA == getPayloadType()) {
            os << OggAnalyzer::opusPacketToString(packet, size, OweOggAnalyzer::parsingOpusSampleRate);
        }

        packet += size;
        if (i >= output_packets_limit) break;
    }
#endif
    return os.str();
}

string OpusIDHeader::toString() const {
    stringstream os;
    os << "magic: ";
    for (auto c: magic) os << c;
    os << dec;
    os << ", version: " << static_cast<int>(version);
    // pre_skip and sample rate are little_endian
    os << ", channel: " << static_cast<int>(channel);
    os << ", pre_skip: " << pre_skip;
    os << dec;
    os << ", sample_rate: " << sample_rate;
    os << ", output_gain: " << output_gain;
    os << ", channel mapping family: " << static_cast<int>(channel_mapping_family);
    return os.str();
}

string OggPagePreHeader::toString() const {
    stringstream os;
    os << "magic: ";
    for (auto &c: this->magic) {
        os << c;
    }
    os << ", version: " << to_string(this->version);

    os << ", type: " << to_string(this->header_type);
    os << ", granule_pos: " << to_string(this->granule_pos);
    os << ", serial number: " << hex << this->serial;
    os << ", sequence: " << this->seq;
    os << ", checksum: " << this->check_sum;
    os << ", num segments: 0x" << hex << static_cast<int>(this->num_segments) << "(" << dec
       << (short) this->num_segments << ")";
    return os.str();
}

int OweOggAnalyzer::parse() {
    if (nullptr == parsingFile) {
        logError("Parsing file not opened failed.");
        return -1;
    }

    OggPage page;
    int pageLimit = 10;
    for (int i = 0; page.read(parsingFile) >= 0 && i < pageLimit; ++i) {
        // read pages
        page.pageIndex = i;
        if (!page.header.isValid()) {
            logWarn("The %dth path is not a valid page, exit...", i);
            break;
        }
        printf("%s\n", page.toString().c_str());
    }

    return 0;
}

OweOggAnalyzer::OweOggAnalyzer(const char *file) : OggAnalyzer(file) {
    parsingFile = fopen(file, "rb");
    if (nullptr == parsingFile) {
        logError("Open parsing file %s failed.", file);
    }
}

OweOggAnalyzer::~OweOggAnalyzer() {
    if (nullptr != parsingFile) {
        fclose(parsingFile);
    }
}
